﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.Linq;
using System.Text;

using LightningTools.BCSTesterMan.Common.ExtensionMethods;

using Microsoft.BusinessData.MetadataModel;
using Microsoft.BusinessData.Runtime;
using Microsoft.SharePoint;
using Microsoft.SharePoint.BusinessData.Administration.Client;
using Microsoft.SharePoint.BusinessData.Administration.Client.Parser;
using Microsoft.SharePoint.BusinessData.Runtime;
using Microsoft.SharePoint.BusinessData.SharedService;

namespace LightningTools.BCSTesterMan.Model.DataAccessLayer
{
    public class BcsHelper
    {
        private const bool ActiveOnly = true;
        static BcsHelper _instance;
        static readonly object Padlock = new object();

        /// <summary>
        /// Collection of Entities
        /// </summary>
        private EntityCollection entityCollection;

        /// <summary>
        /// Initializes a new instance of the <see cref="BcsHelper"/> class.
        /// </summary>
        private BcsHelper()
        {
        }

        /// <summary>
        /// Gets or sets the URL.
        /// </summary>
        /// <value>The URL.</value>
        public string Url { private get; set; }

        /// <summary>
        /// Gets or sets the row limit.
        /// </summary>
        /// <value>The row limit.</value>
        public int RowLimit { private get; set; }

        /// <summary>
        /// Gets the instance.
        /// </summary>
        /// <value>The instance.</value>
        public static BcsHelper Instance
        {
            get
            {
                lock (Padlock)
                {
                    return _instance ?? (_instance = new BcsHelper());
                }
            }
        }

        /// <summary>
        /// Gets the entities.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<Entity> GetEntities()
        {
            AdministrationMetadataCatalog catalog = AdministrationMetadataCatalog.GetCatalog(Url);

            // Get all Entities
            entityCollection = catalog.GetEntities("*", "*", ActiveOnly);

            // Create custom Entity for each SharePoint Entity
            IEnumerable<Entity> simpleEntities = (from entity in entityCollection
                                                  select new Entity
                                                  {
                                                      Id = entity.Id,
                                                      Name = entity.Name,
                                                      Version = entity.Version.ToString(),
                                                      Namespace = entity.Namespace,
                                                      // Load Methods for the Entity
                                                      Methods = (entity.Methods.Select(m => new Method
                                                      {
                                                          Name = m.Name,
                                                          Id = m.Id,
                                                          ParentEntityId = entity.Id,
                                                          MethodType = m.MethodInstances.First().MethodInstanceType.ToString(),
                                                          IsSupported = CheckSupportedMethodType(m.MethodInstances.First().MethodInstanceType.ToString())
                                                         // Parameters = GetInputParameters(m)
                                                      })).ToObservableCollection(),

                                                      // Load the XML Model
                                                      Model = entity.WriteXml(PackageContents.Model)
                                                  });

            return simpleEntities;
        }

        /// <summary>
        /// Checks if the Method Type is supported.
        /// </summary>
        /// <param name="methodType">Type of the method.</param>
        /// <returns></returns>
        private static bool CheckSupportedMethodType(string methodType)
        {
            var type = (MethodInstanceType)Enum.Parse(typeof(MethodInstanceType), methodType);

            bool isSupported;

            switch (type)
            {
                case MethodInstanceType.Finder:
                case MethodInstanceType.SpecificFinder:
                case MethodInstanceType.IdEnumerator:
                case MethodInstanceType.Updater:
                case MethodInstanceType.Creator:
                case MethodInstanceType.Deleter:
                    isSupported = true;
                    break;
                default:
                    isSupported = false;
                    break;
            }

            return isSupported;
        }


        /// <summary>
        /// Invokes the method.
        /// </summary>
        /// <param name="simpleMethod">The simple method.</param>
        /// <param name="offsetFromStart">The offset from start.</param>
        /// <param name="rowCount">The row count.</param>
        /// <returns></returns>
        public MethodResponseDetail InvokeMethod(Method simpleMethod, int offsetFromStart, int rowCount)
        {
            // Get Entity from the Method
            Microsoft.SharePoint.BusinessData.Administration.Client.Entity entity = entityCollection.Single(e => e.Id == simpleMethod.ParentEntityId);

            // Get the method from the Entity
            Microsoft.SharePoint.BusinessData.Administration.Client.Method method = entity.Methods.Single(m => m.Id == simpleMethod.Id);

            return ExecuteMethod(entity, method, simpleMethod.Parameters, offsetFromStart, rowCount);
        }


        /// <summary>
        /// Invokes the method.
        /// </summary>
        /// <param name="simpleMethod">The simple method.</param>
        /// <returns></returns>
        public MethodResponseDetail InvokeMethod(Method simpleMethod)
        {
            // Get Entity from the Method
            Microsoft.SharePoint.BusinessData.Administration.Client.Entity entity =
                entityCollection.Single(e => e.Id == simpleMethod.ParentEntityId);

            // Get the method from the Entity
            Microsoft.SharePoint.BusinessData.Administration.Client.Method method =
                entity.Methods.Single(m => m.Id == simpleMethod.Id);

            // Execute method with Default Row and Page values
            return ExecuteMethod(entity, method, simpleMethod.Parameters, 0, 100);
        }

        /// <summary>
        /// Executes the method.
        /// </summary>
        /// <param name="myEntity">My entity.</param>
        /// <param name="myMethod">My method.</param>
        /// <param name="parameters">The parameters.</param>
        /// <param name="offsetFromStart">The offset from start.</param>
        /// <param name="rowCount">The row count.</param>
        /// <returns></returns>
        private MethodResponseDetail ExecuteMethod(
            Microsoft.SharePoint.BusinessData.Administration.Client.Entity myEntity,
            Microsoft.SharePoint.BusinessData.Administration.Client.Method myMethod, IEnumerable<Parameter> parameters,
            int offsetFromStart, int rowCount)
        {
            MethodResponseDetail responseDetail = null;

            // Get the first MethodInstance
            // TODO: This will need to support methods with multiple instances
            string methodInstanceName = myMethod.MethodInstances.First().Name;

            // Get the Method Instance type
            // TODO: This will need to support methods with multiple instances
            MethodInstanceType methodInstanceType = myMethod.MethodInstances.First().MethodInstanceType;

            using (var site = new SPSite(Url))
            {
                using (new SPServiceContextScope(SPServiceContext.GetContext(site)))
                {
                    // Get BDC Service
                    var service = site.WebApplication.Farm.Services.GetValue<BdcService>();

                    // Get the Catalog
                    IMetadataCatalog catalog = service.GetDatabaseBackedMetadataCatalog(SPServiceContext.Current);

                    // Get the Entity from our Custom Entity
                    IEntity entity = catalog.GetEntity(myEntity.Namespace, myEntity.Name);

                    // Get LobSystem Instance
                    // TODO: This will need to support LobSystem with multiple LobSystem instances
                    ILobSystemInstance lobSysteminstance = entity.GetLobSystem().GetLobSystemInstances()[0].Value;

                    // Get the SharePoint Method
                    IMethodInstance methodInstance = entity.GetMethodInstance(methodInstanceName, methodInstanceType);

                    // Execute the method depending on the Type.  
                    switch (methodInstanceType)
                    {
                        case MethodInstanceType.Finder:
                            {
                                responseDetail = ParseFinderMethod(entity, methodInstance, lobSysteminstance, parameters,
                                    offsetFromStart, rowCount);
                            }
                            break;
                        case MethodInstanceType.SpecificFinder:
                            {
                                responseDetail = ParseSpecificFinderMethod(entity, parameters);
                            }
                            break;
                        case MethodInstanceType.IdEnumerator:
                            {
                                responseDetail = ParseIdEnumeratorMethod(entity, methodInstance, lobSysteminstance,
                                    offsetFromStart, rowCount);
                            }
                            break;
                        case MethodInstanceType.Updater:
                            {
                                responseDetail = ExecuteUpdaterMethod(entity, parameters);
                            }
                            break;
                        case MethodInstanceType.Creator:
                            {
                                responseDetail = ExecuteCreatorMethod(entity, parameters, methodInstance, lobSysteminstance);
                            }
                            break;
                        case MethodInstanceType.Deleter:
                            {
                                responseDetail = ExecuteDeleterMethod(entity, parameters);
                            }
                            break;
                    }
                }
            }

            return responseDetail;
        }

        /// <summary>
        /// Executes the deleter method.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="parameters">The parameters.</param>
        /// <returns></returns>
        private static MethodResponseDetail ExecuteDeleterMethod(IEntity entity, IEnumerable<Parameter> parameters)
        {
            // Create new identity 
            Identity identity = GetIdentity(parameters);

            // Execute specific finder method and get the entity instance  
            IEntityInstance entInstance = entity.FindSpecific(identity,
                entity.GetLobSystem().GetLobSystemInstances()[0].Value);

            entInstance.Delete();

            return new MethodResponseDetail("Deleter Method Successfully Completed");
        }

        /// <summary>
        /// Executes the creator method.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="parameters">The parameters.</param>
        /// <param name="methodInstance">The method instance.</param>
        /// <param name="lobSysteminstance">The lob systeminstance.</param>
        /// <returns></returns>
        private static MethodResponseDetail ExecuteCreatorMethod(IEntity entity, IEnumerable<Parameter> parameters,
            IMethodInstance methodInstance, ILobSystemInstance lobSysteminstance)
        {
            // Get Creator View
            IView creatorView = entity.GetCreatorView(methodInstance.Name);

            // Get Default Values
            IFieldValueDictionary defaultValues = creatorView.GetDefaultValues();

            // Loop through parameters and set the values
            foreach (Parameter parameter in parameters)
            {

                object convertedType = GetParameterConvertedType(parameter);

                // Set the fields value
                defaultValues[parameter.FieldAppliedTo] = convertedType;
            }

            // Create the data and return the Identity
            Identity identity = entity.Create(defaultValues, lobSysteminstance);

            // Call the Specific Finder using the Identity from the newly created item
            MethodResponseDetail methodResponseDetail = ParseSpecificFinderMethod(entity, identity);

            methodResponseDetail.Detail = "Creator Method Successfully Completed";
            return methodResponseDetail;
        }

        /// <summary>
        /// Executes the updater method.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="parameters">The parameters.</param>
        /// <returns></returns>
        private static MethodResponseDetail ExecuteUpdaterMethod(IEntity entity, IEnumerable<Parameter> parameters)
        {
            // Create new identity 
            Identity identity = GetIdentity(parameters);

            // Execute specific finder method and get the entity instance  
            IEntityInstance entInstance = entity.FindSpecific(identity,
                entity.GetLobSystem().GetLobSystemInstances()[0].Value);

            var fieldInfo = new StringBuilder();
            fieldInfo.AppendLine("Updater Method details:");

            // Loop through each configured Parameter
            foreach (Parameter parameter in parameters)
            {
                // Write Field Info for logging
                fieldInfo.AppendLine(string.Format("{0}: ", parameter.FieldAppliedTo));

                // Don't update read only property
                if (!parameter.IsReadOnly)
                {
                    string previousValue = entInstance[parameter.FieldAppliedTo].ToString();

                    object paramConvertedType = GetParameterConvertedType(parameter);

                    // Set the Fields value to the parameter value
                    entInstance[parameter.FieldAppliedTo] = paramConvertedType;

                    fieldInfo.AppendFormat("{0} -> {1}", previousValue, entInstance[parameter.FieldAppliedTo]);
                }
            }

            // Update the instance
            entInstance.Update();

            fieldInfo.AppendLine("Finder Method Successfully Completed");

            MethodResponseDetail methodResponseDetail = ParseSpecificFinderMethod(entity, identity);
            methodResponseDetail.Detail = fieldInfo.ToString();
            return methodResponseDetail;
        }

        /// <summary>
        /// Gets the identity.
        /// </summary>
        /// <param name="parameters">The parameters.</param>
        /// <returns></returns>
        private static Identity GetIdentity(IEnumerable<Parameter> parameters)
        {
            // Gets the identifier from the list of parameters
            // TODO: Add support if there is multiple Identifiers
            object identifier = GetParameterConvertedType(parameters.First(p => p.IsIdentifier));
            return new Identity(identifier);
        }

        /// <summary>
        /// Gets the type of the parameter converted.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns></returns>
        private static object GetParameterConvertedType(Parameter parameter)
        {
            Type paramType = parameter.Type;
            object paramValue = parameter.Value;
            return Convert.ChangeType(paramValue, paramType);
        }

        /// <summary>
        /// Parses the finder method.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="methodInstance">The method instance.</param>
        /// <param name="lobSysteminstance">The lob systeminstance.</param>
        /// <param name="parameters">The parameters.</param>
        /// <param name="offsetFromStart">The offset from start.</param>
        /// <param name="rowCount">The row count.</param>
        /// <returns></returns>
        private static MethodResponseDetail ParseFinderMethod(IEntity entity, IMethodInstance methodInstance,
            ILobSystemInstance lobSysteminstance, IEnumerable<Parameter> parameters, int offsetFromStart, int rowCount)
        {
            DataTable resultsTable;
            IFilterCollection filterCollection = methodInstance.GetFilters();

            // Loop through each filter
            foreach (IFilter filter in filterCollection)
            {
                // Get the field the filter applies to
                string filterField = filter.FilterDescriptor.FilterField;

                switch (filter.FilterDescriptor.Type)
                {
                    case FilterType.Comparison:
                        {
                            var wfilter = (ComparisonFilter)filter;
                            wfilter.Value = parameters.Single(p => p.FilterField == filterField).Value;
                        }
                        break;
                    case FilterType.Wildcard:
                        {
                            var wfilter = (WildcardFilter)filter;
                            wfilter.Value = parameters.Single(p => p.FilterField == filterField).Value;
                        }
                        break;
                    case FilterType.Limit:
                        {
                            var limitFilter = (LimitFilter)filter;
                            limitFilter.Value =
                                Convert.ToUInt32(parameters.Single(p => p.FilterField == filterField).Value);
                        }
                        break;
                }
            }

            // Execute finder method  
            using (IEntityInstanceEnumerator ieie = entity.FindFiltered(filterCollection, lobSysteminstance))
            {
                resultsTable = entity.Catalog.Helper.CreateDataTable(ieie, offsetFromStart, rowCount);
            }

            return new MethodResponseDetail("Finder Method Successfully Completed", resultsTable);
        }

        /// <summary>
        /// Parses the specific finder method.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="parameters">The parameters.</param>
        /// <returns></returns>
        private static MethodResponseDetail ParseSpecificFinderMethod(IEntity entity, IEnumerable<Parameter> parameters)
        {
            // Create new identity 
            Identity identity = GetIdentity(parameters);

            // Execute specific finder method and get the entity instance  
            IEntityInstance entInstance = entity.FindSpecific(identity,
                entity.GetLobSystem().GetLobSystemInstances()[0].Value);

            return new MethodResponseDetail("Specific Finder Method Successfully Completed",
                entInstance.EntityAsFormattedDataTable);
        }

        /// <summary>
        /// Parses the specific finder method.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="identity">The identity.</param>
        /// <returns></returns>
        private static MethodResponseDetail ParseSpecificFinderMethod(IEntity entity, Identity identity)
        {
            // Execute specific finder method and get the entity instance  
            IEntityInstance entInstance = entity.FindSpecific(identity,
                entity.GetLobSystem().GetLobSystemInstances()[0].Value);

            return new MethodResponseDetail("Specific Finder Method Successfully Completed",
                entInstance.EntityAsFormattedDataTable);
        }

        /// <summary>
        /// Parses the id enumerator method.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="methodInstance">The method instance.</param>
        /// <param name="lobSysteminstance">The lob systeminstance.</param>
        /// <param name="offsetFromStart">The offset from start.</param>
        /// <param name="rowCount">The row count.</param>
        /// <returns></returns>
        private static MethodResponseDetail ParseIdEnumeratorMethod(IEntity entity, IMethodInstance methodInstance,
            ILobSystemInstance lobSysteminstance, int offsetFromStart, int rowCount)
        {
            DataTable idEnumeratorDataTable;

            // Execute IdEnumerator method  
            using (
                IEntityInstanceEnumerator ieie = entity.EnumerateIdentities(methodInstance.GetFilters(),
                    lobSysteminstance))
            {
                idEnumeratorDataTable = entity.Catalog.Helper.CreateDataTable(ieie, offsetFromStart, rowCount);
            }

            return new MethodResponseDetail("Id Enumerator Method Successfully Completed", idEnumeratorDataTable);
        }

        /// <summary>
        /// Gets the input parameters.
        /// </summary>
        /// <param name="simpleMethod">The simple method.</param>
        /// <param name="url">The URL.</param>
        /// <returns></returns>
        public static IEnumerable<Parameter> GetInputParameters(Method simpleMethod, string url)
        {
            AdministrationMetadataCatalog catalog = AdministrationMetadataCatalog.GetCatalog(url);
            EntityCollection entityCollection = catalog.GetEntities("*", "*", ActiveOnly);

            Microsoft.SharePoint.BusinessData.Administration.Client.Entity entity =
                entityCollection.Single(e => e.Id == simpleMethod.ParentEntityId);
            Microsoft.SharePoint.BusinessData.Administration.Client.Method method =
                entity.Methods.Single(m => m.Id == simpleMethod.Id);

            IEnumerable<Microsoft.SharePoint.BusinessData.Administration.Client.Parameter> inParameters =
                method.Parameters.Where(p => p.Direction == DirectionType.In);

            return inParameters.Select(parameter => new Parameter
            {
                Name = parameter.Name,
                Type = Type.GetType(parameter.RootTypeDescriptor.TypeName),
                FieldAppliedTo = parameter.RootTypeDescriptor.Name,
                Filter =
                    parameter.RootTypeDescriptor.ContainsFilterDescriptor
                        ? parameter.RootTypeDescriptor.FilterDescriptor.Type.ToString()
                        : "Input Parameter",
                FilterField =
                    parameter.RootTypeDescriptor.ContainsFilterDescriptor
                        ? parameter.RootTypeDescriptor.FilterDescriptor.FilterField
                        : null,
                IsReadOnly = parameter.RootTypeDescriptor.IdentifierReference != null,
                IsIdentifier = parameter.RootTypeDescriptor.IdentifierReference != null,
                Value = parameter.RootTypeDescriptor.GetDefaultValue(method.MethodInstances.First().Id)
            });
        }

        /// <summary>
        /// Gets the input parameters.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <returns></returns>
        public static ObservableCollection<Parameter> GetInputParameters(Microsoft.SharePoint.BusinessData.Administration.Client.Method method)
        {
            IEnumerable<Microsoft.SharePoint.BusinessData.Administration.Client.Parameter> inParameters =
                    method.Parameters.Where(p => p.Direction == DirectionType.In);

            return inParameters.Select(parameter => new Parameter
            {
                Name = parameter.Name,
                Type = Type.GetType(parameter.RootTypeDescriptor.TypeName),
                FieldAppliedTo = parameter.RootTypeDescriptor.Name,
                Filter =
                    parameter.RootTypeDescriptor.ContainsFilterDescriptor
                        ? parameter.RootTypeDescriptor.FilterDescriptor.Type.ToString()
                        : "Input Parameter",
                FilterField =
                    parameter.RootTypeDescriptor.ContainsFilterDescriptor
                        ? parameter.RootTypeDescriptor.FilterDescriptor.FilterField
                        : null,
                IsReadOnly = parameter.RootTypeDescriptor.IdentifierReference != null,
                IsIdentifier = parameter.RootTypeDescriptor.IdentifierReference != null,
                Value = parameter.RootTypeDescriptor.GetDefaultValue(method.MethodInstances.First().Id)
            }).ToObservableCollection();
        }
    }
}